use sentry_core::protocol as proto;
use std::{path::Path, time::SystemTime};

pub use breakpad_handler::InstallOptions;

/// Monitors the current process for crashes, writing them to disk as minidumps
/// and reporting the crash event to Sentry.
pub struct BreakpadIntegration {
    crash_handler: Option<breakpad_handler::BreakpadHandler>,
}

impl BreakpadIntegration {
    /// Creates a new Breakpad Integration, note that only *one* can exist
    /// in the application at a time!
    pub fn new(
        crash_dir: impl AsRef<Path>,
        install_options: InstallOptions,
        hub: std::sync::Arc<sentry_core::Hub>,
    ) -> Result<Self, crate::Error> {
        // The paths generated by breakpad are just guids with an extension so they
        // are utf-8 safe, however, due to how we pass the path via metadata
        // to the transport, we require that the root crash path is also utf-8
        if crash_dir.as_ref().to_str().is_none() {
            return Err(crate::Error::NonUtf8Path(crash_dir.as_ref().to_owned()));
        }

        // Ensure the directory exists, breakpad should do this when writing crashdumps
        // anyway, but then again, it's C++ code, so I have low trust
        std::fs::create_dir_all(&crash_dir)?;

        let crash_hub = std::sync::Arc::downgrade(&hub);
        let crash_handler = breakpad_handler::BreakpadHandler::attach(
            &crash_dir,
            install_options,
            Box::new(move |minidump_path: std::path::PathBuf| {
                if let Some(crash_hub) = crash_hub.upgrade() {
                    // We **don't** do end_session_with_status as it just
                    // immediately takes the session from the scope and sends it,
                    // but we want to send the event, session update, and minidump
                    // all in the same event
                    // crash_hub.end_session_with_status(protocol::SessionStatus::Crashed);

                    let mut extra = std::collections::BTreeMap::new();
                    // We should never get here unless the path is valid utf-8, so this is fine
                    extra.insert(
                        "__breakpad_minidump_path".to_owned(),
                        minidump_path
                            .to_str()
                            .expect("utf-8 path")
                            .to_owned()
                            .into(),
                    );

                    // Create an event for crash so that we can add all of the context
                    // we can to it, the important information like stack traces/threads
                    // modules/etc is contained in the minidump recorded by breakpad
                    let event = proto::Event {
                        level: proto::Level::Fatal,
                        // We want to set the timestamp here since we aren't actually
                        // going to send the crash directly, but rather the next time
                        // this integration is initialized
                        timestamp: SystemTime::now(),
                        // This is the easiest way to indicate a session crash update
                        // in the same envelope with the crash itself. :p
                        exception: vec![proto::Exception {
                            mechanism: Some(proto::Mechanism {
                                handled: Some(false),
                                ..Default::default()
                            }),
                            ..Default::default()
                        }]
                        .into(),
                        extra,
                        ..Default::default()
                    };

                    crash_hub.capture_event(event);

                    if let Some(client) = crash_hub.client() {
                        client.close(None);
                    }
                }
            }),
        )?;

        let crash_dir = crash_dir.as_ref().to_owned();

        Self::upload_minidumps(&crash_dir, &hub);

        Ok(Self {
            crash_handler: Some(crash_handler),
        })
    }

    /// Called during startup to send any minidumps + metadata that have been
    /// captured in previous sessions but (seem to) have not been sent yet
    fn upload_minidumps(crash_dir: &Path, hub: &sentry_core::Hub) {
        // Scan the directory the integration was initialized with to find any
        // envelopes that have been serialized to disk and send + delete them
        let rd = match std::fs::read_dir(crash_dir) {
            Ok(rd) => rd,
            Err(e) => {
                debug_print!(
                    "Unable to read crash directory '{}': {}",
                    crash_dir.display(),
                    e
                );
                return;
            }
        };

        let client = match hub.client() {
            Some(c) => c,
            None => return,
        };

        // The minidumps are what we care about the most, but of course, the
        // metadata that we (hopefully) were able to capture along with the crash
        for entry in rd.filter_map(|e| e.ok()) {
            if entry
                .file_name()
                .to_str()
                .map_or(true, |s| !s.ends_with(".dmp"))
            {
                continue;
            }

            let mut minidump_path = entry.path();
            minidump_path.set_extension("metadata");

            let md = crate::shared::CrashMetadata::deserialize(&minidump_path);
            if let Err(e) = std::fs::remove_file(&minidump_path) {
                debug_print!("failed to remove {}: {}", minidump_path.display(), e);
            }

            minidump_path.set_extension("dmp");

            let envelope = crate::shared::assemble_envelope(md, &minidump_path);
            if let Err(e) = std::fs::remove_file(&minidump_path) {
                debug_print!("failed to remove {}: {}", minidump_path.display(), e);
            }

            client.send_envelope(envelope);
        }
    }

    #[inline]
    pub fn inner_handler(&self) -> &Option<breakpad_handler::BreakpadHandler> {
        &self.crash_handler
    }
}

impl Drop for BreakpadIntegration {
    fn drop(&mut self) {
        let _ = self.crash_handler.take();
    }
}
